﻿using SAIM.Models.DAL;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web;

namespace SAIM.Models.BLL
{
    public class ListStructureBLL : IDisposable
    {
        private eVirtualEntities db;
        private string ValidationErrors;

        public ListStructureBLL()
        {
            db = new eVirtualEntities();
            ValidationErrors = string.Empty;
        }

        /// <summary>
        /// Agrega una nueva columna a la estructura de una lista
        /// </summary>
        /// <param name="column">Columna a agregar</param>
        /// <returns>Devuelve verdadero si la columna se insertó exitosamente</returns>
        public bool InsertColumn(ColumnData column, bool alterTable = true)
        {
            bool success = false;
            try
            {
                column.SetDefaults();
                // Generamos la nueva columna
                ListStructureDAL columnDAL = new ListStructureDAL()
                {
                    ColumnName = column.ColumnName,
                    ListId = column.ListId,
                    ColumnType = column.DataType,
                    IsRequired = column.Required,
                    IsUnique = column.Unique,
                    ColumnDesc = column.Description,
                    DefaultValue = column.DefaultValue,
                    ChoiceType = column.ChoiceType,
                    MinValue = column.MinSize,
                    MaxValue = column.MaxSize,
                    DecimalPlaces = column.DecimalPlaces,
                    Size = column.Size,
                    CreationDate = DateTime.Now
                };
                if(alterTable)
                    AlterTable(column); // Modificación de la tabla correspondiente a la columna a agregar en la Base de Datos
                db.ListStructure.Add(columnDAL);
                db.SaveChanges();
                success = true;
            }
            catch (Exception) { success = false; }
            return success;
        }

        /// <summary>
        /// Agrega una nueva columna a la tabla indicada dentro de la columna a agregar
        /// </summary>
        /// <param name="column">Columna a agregar</param>
        /// <returns></returns>
        private bool AlterTable(ColumnData column)
        {
            bool success;
            string columName;
            string listId;
            string command = "ALTER TABLE {0} ADD {1} {2} NULL {3}";

            // Verificamos el tipo de dato a insertar para proceder a generar el comando encargado de agregar la columna
            switch(column.DataType)
            {
                case "Text":
                    command = String.Format(command,
                        "DataAppList." + column.ListId,
                        column.ColumnName.Replace(" ", ""),
                        "VARCHAR(" + column.Size.ToString() + ")",
                        column.DefaultValue != null ? "DEFAULT '" + column.DefaultValue + "'" : string.Empty);
                    break;
                case "Choice":
                    columName = column.ColumnName.Replace(" ", "");
                    listId = column.ListId;
                    List<string> choices = System.Text.RegularExpressions.Regex.Split(column.Choices, @"\r\n").ToList();
                    if (column.DefaultValue != null && choices.FirstOrDefault(a => a == column.DefaultValue) == null)
                        choices.Add(column.DefaultValue);
                    AddChoices(choices, columName, listId);
                    command = String.Format(command,
                        "DataAppList." + column.ListId,
                        columName,
                        "VARCHAR(255)",
                        column.DefaultValue != null ? "DEFAULT '" + column.DefaultValue + "'" : string.Empty);
                    break;
                case "Integer":
                    command = String.Format(command,
                        "DataAppList." + column.ListId,
                        column.ColumnName.Replace(" ", ""),
                        "BIGINT",
                        column.DefaultValue != null ? "DEFAULT " + column.DefaultValue : string.Empty);
                    break;
                case "Decimal":
                    command = String.Format(command,
                        "DataAppList." + column.ListId,
                        column.ColumnName.Replace(" ", ""),
                        "DECIMAL(" + (column.DecimalPlaces + 19).ToString() + "," + column.DecimalPlaces + ")",
                        column.DefaultValue != null ? "DEFAULT " + column.DefaultValue : string.Empty);
                    break;
                case "Date":
                    command = String.Format(command,
                        "DataAppList." + column.ListId,
                        column.ColumnName.Replace(" ", ""),
                        "DATE",
                        column.DefaultValue != null ? "DEFAULT '" + column.DefaultValue + "'" : string.Empty);
                    break;
                case "Time":
                    command = String.Format(command,
                        "DataAppList." + column.ListId,
                        column.ColumnName.Replace(" ", ""),
                        "TIME",
                        column.DefaultValue != null ? "DEFAULT '" + column.DefaultValue + "'" : string.Empty);
                    break;
                case "DateTime":
                    command = String.Format(command,
                        "DataAppList." + column.ListId,
                        column.ColumnName.Replace(" ", ""),
                        "DATETIME",
                        column.DefaultValue != null ? "DEFAULT '" + column.DefaultValue + "'" : string.Empty);
                    break;
                case "Hiperlink":
                    command = String.Format(command,
                        "DataAppList." + column.ListId,
                        column.ColumnName.Replace(" ", ""),
                        "VARCHAR(512)",
                        column.DefaultValue != null ? "DEFAULT '" + column.DefaultValue + "'" : string.Empty);
                    break;
            }
            try
            {
                db.Database.ExecuteSqlCommand(command); // Ejecutamos el comando para la creacion de la columna
                success = true;
            }
            catch (Exception) { success = false; }
            return success;
        }

        /// <summary>
        /// Agrega un conjunto de elecciones para una columna determinada por su nombre
        /// </summary>
        /// <param name="choices">Elecciones a agregar</param>
        /// <param name="columnName">Nombre de la columna a asociar</param>
        /// <returns></returns>
        private bool AddChoices(List<string> choices, string columnName, string listId)
        {
            bool success;
            string command;

            try
            {
                command = "INSERT INTO SystemAppList.Choices (ColumnName, ChoiceName, ListId) VALUES ";
                foreach(string choice in choices)
                {
                    command += "('" + columnName + "', '" + choice.Replace("'", "''") + "', '" + listId + "'),";
                }
                command = command.Remove(command.Length - 1) + ";";
                db.Database.ExecuteSqlCommand(command);
                success = true;
            }
            catch (Exception) { success = false; }
            return success;
        }

        /// <summary>
        /// Devuelve una lista con los nombres de las columnas en una lista específica
        /// </summary>
        /// <param name="listId">Nombre de la lista</param>
        /// <returns></returns>
        public List<string> GetListStructure(string listId)
        {
            return (from column in db.ListStructure
                    where column.ListId == listId
                    orderby column.CreationDate ascending 
                    select column.ColumnName).ToList();
        }

        /// <summary>
        /// Elimina la columna de una lista específica
        /// </summary>
        /// <param name="listId">Nombre de la lista</param>
        /// <param name="columnName">Nombre de la columna</param>
        /// <returns></returns>
        public bool RemoveColumn(string listId, string columnName)
        {
            bool success;
            ListStructureDAL column;
            try
            {
                // Buscamos la columna dentro de la lista 
                column = db.ListStructure.FirstOrDefault(c => c.ListId == listId && c.ColumnName == columnName);
                if(column != null)
                {
                    // Si la columna existe la eliminamos de la tabla a la que pertenece y la eliminamos de la tabla ListStructure
                    if (RemoveDataBaseColumn(listId, columnName.Replace(" ", ""), column.ColumnType, ((column.DefaultValue != string.Empty) && (column.DefaultValue != null))))
                    {
                        db.ListStructure.Remove(column);
                        db.SaveChanges();
                        success = true;
                    }
                    else
                        success = false;
                }
                else
                {
                    success = false;
                }
            }
            catch (Exception) { success = false; }
            return success;
        }

        /// <summary>
        /// Elimina la columna de una lista específica. (Eliminación de la columna en la base de datos)
        /// </summary>
        /// <param name="tableName">Nombre de la tabla (Lista)</param>
        /// <param name="columnName">Nombre de la columna</param>
        /// <param name="columnType">Tipo de columna a eliminar</param>
        /// <returns></returns>
        public bool RemoveDataBaseColumn(string tableName, string columnName, string columnType, bool hasDefaultValue)
        {
            bool success = false;
            string command;

            try
            {
                // Eliminamos las opciones para esta columna en caso de ser de tipo "Choice"
                if (columnType == "Choice")
                {
                    RemoveChoices(columnName, tableName.Replace(" ", ""));
                }
                // Eliminamos la restriccion DEFAULT en caso de contar con alguna
                if (hasDefaultValue)
                {
                    command = "SELECT df.name 'Constraint' FROM sys.default_constraints df" +
                        " INNER JOIN sys.tables t ON df.parent_object_id = t.object_id" +
                        " INNER JOIN sys.columns c ON df.parent_object_id = c.object_id AND df.parent_column_id = c.column_id" +
                        " WHERE t.name = '" + tableName + "' AND  c.NAME = '" + columnName + "'";
                    string defaultKey = db.Database.SqlQuery<string>(command).First();
                    command = "ALTER TABLE DataAppList.{0} DROP constraint [{1}]";
                    command = String.Format(command, tableName, defaultKey);
                    db.Database.ExecuteSqlCommand(command);
                }
                // Eliminamos la columna
                command = "ALTER TABLE DataAppList.{0} DROP COLUMN {1}";
                command = String.Format(command, tableName, columnName);
                db.Database.ExecuteSqlCommand(command);
                success = true;
            }
            catch (Exception) { }
            return success;
        }

        /// <summary>
        /// Elimina las opciones para una columna
        /// </summary>
        /// <param name="columnName">Nombre de la columna</param>
        /// <returns></returns>
        public bool RemoveChoices(string columnName, string listId)
        {
            bool success = false;
            string command;

            try
            {
                // Eliminamos las opciones de la columna
                command = "DELETE FROM SystemAppList.Choices WHERE ColumnName = '{0}' AND ListId = '{1}'";
                command = String.Format(command, columnName.Replace(" ", ""), listId);
                db.Database.ExecuteSqlCommand(command);
                success = true;
            }
            catch (Exception) { }
            return success;
        }

        /// <summary>
        /// Valida los elementos de un diccionario en base a la estructura de una lista
        /// </summary>
        /// <param name="model">Diccionario a validar</param>
        /// <returns></returns>
        public string ValidateDictionary(Dictionary<string, string> model)
        {
            string listId;
            Dictionary<string, string> itemsToAdd;

            itemsToAdd = new Dictionary<string, string>();
            // Buscamos el ID de la lista en la que el diccionario (Tupla) sera insertado 
            listId = model.FirstOrDefault(m => m.Key == "ListId").Value; 
            if (listId != null)
            {
                using (ListsBLL list = new ListsBLL())
                {
                    List<ListStructure> structure;
                    // Buscamos la estructura de la lista
                    structure = list.GetListStructure(listId);
                    // Iteramos los elementos de la estructura para poder hacer las validaciones
                    // NOTA: CADA ELEMENTO ITERADO REPRESENTA UNA COLUMNA DENTRO DE LA LISTA
                    foreach (ListStructure column in structure)
                    {
                        string errors = string.Empty;
                        // Encontramos el valor correspondiente al elemento que estamos iterando
                        string itemVal = model.FirstOrDefault(m => m.Key == column.ColumnName).Value;
                        // Si el valor es una cadena vacia o nulo lo igualamos a nulo
                        itemVal = ((itemVal == null) || (itemVal.Trim() == string.Empty)) ? null : itemVal;
                        // Si la columna cuenta con un valor por defecto y el elemento a agregar es nulo se le asigna el valor por defecto
                        if (column.DefaultValue != null)
                        {
                            itemVal = itemVal ?? column.DefaultValue;
                        }
                        #region Validación de campo requerido
                        if (column.IsRequired)
                        {
                            if ((itemVal == null) || (itemVal.Trim() == string.Empty))
                            {
                                errors += SAIM.Resources.Validations.FieldRequired;
                            }
                        }
                        #endregion 
                        #region Validación mediante tipos de dato
                        if ((itemVal != null) && (itemVal.Trim() != string.Empty))
                        {
                            switch(column.ColumnType)
                            {
                                #region Texto
                                case "Text":
                                    if(column.Size != null)
                                        if (itemVal.Length > column.Size)
                                            itemVal = itemVal.Substring(0, column.Size.Value);
                                    break;
                                #endregion
                                #region Elección (Choice)
                                case "Choice":
                                    // En caso de que el elmento a agregar se de tipo Choice 
                                    // validamos que cuando menos un elemento se encuentre seleccionado
                                    switch(column.ChoiceType)
                                    {
                                        case "Checkbox":
                                            string[] choices = Newtonsoft.Json.JsonConvert.DeserializeObject<string[]>(itemVal);
                                            if (choices.Length > 0)
                                            {
                                                if (column.Choices.FirstOrDefault(c => choices.FirstOrDefault(a => a == c) != null) == null)
                                                {
                                                    errors += "\n" + SAIM.Resources.Validations.AtLeastOneChoice;
                                                }
                                            }
                                            else
                                            {
                                                model[column.ColumnName] = string.Empty;
                                                if(column.IsRequired)
                                                    errors += "\n" + SAIM.Resources.Validations.FieldRequired;
                                            }
                                            break;
                                    }
                                    break;
                                #endregion
                                #region Entero
                                case "Integer":
                                    double intValue;
                                    if (Double.TryParse(itemVal, out intValue))
                                    {
                                        if (column.MinValue != null) // Validacion mediante valor minimo permitido
                                            if (column.MinValue.Value > intValue)
                                                errors += "\n" + String.Format(SAIM.Resources.Validations.Min, column.MinValue.Value.ToString());
                                        if (column.MaxValue != null)
                                            if (column.MaxValue.Value < intValue) // Validacion mediante valor maximo permitido
                                                errors += "\n" + String.Format(SAIM.Resources.Validations.Max, column.MaxValue.Value.ToString());
                                    }
                                    else
                                        errors += "\n" + SAIM.Resources.Validations.Number;
                                    break;
                                #endregion
                                #region Decimal
                                case "Decimal":
                                    double decValue;
                                    int decimalPlaces = column.DecimalPlaces ?? 3;
                                    if (Double.TryParse(itemVal, out decValue))
                                    {
                                        decValue = TruncateDouble(decValue, decimalPlaces); // Solo consideramos los puntos decimales indicados por la columna (Por defecto 3)
                                        if (column.MinValue != null)
                                            if (column.MinValue.Value > decValue) // Validacion mediante valor minimo permitido
                                                errors += "\n" + String.Format(SAIM.Resources.Validations.Min, column.MinValue.Value.ToString());
                                        if (column.MaxValue != null)
                                            if (column.MaxValue.Value < decValue) // Validacion mediante valor maximo permitido
                                                errors += "\n" + String.Format(SAIM.Resources.Validations.Max, column.MaxValue.Value.ToString());
                                    }
                                    else
                                        errors += "\n" + SAIM.Resources.Validations.Number;
                                    break;
                                #endregion
                                #region Hora
                                case "Time":
                                    DateTime time;
                                    if(!DateTime.TryParse(DateTime.Now.ToShortDateString() + " " + itemVal, out time))
                                    {
                                        errors += "\n" + SAIM.Resources.Validations.Time;
                                    }
                                    break;
                                #endregion
                                #region Hipervínculo
                                case "Hiperlink":
                                    if(!Uri.IsWellFormedUriString(itemVal, UriKind.RelativeOrAbsolute))
                                    {
                                        errors += "\n" + SAIM.Resources.Validations.Url;
                                    }
                                    break;
                                #endregion
                            }
                        }
                        #endregion
                        if (errors != string.Empty)
                            ValidationErrors += column.Label + ": " + errors + "\n";
                    }
                }
            }
            else
            {
                ValidationErrors += SAIM.Resources.Site.ListName + ": " + SAIM.Resources.Validations.FieldRequired + "\n";
            }

            return ValidationErrors;
        }

        /// <summary>
        /// Agrega una nueva tupla dentro de la lista indicada por el modelo
        /// </summary>
        /// <param name="model">Tupla a insertar</param>
        /// <returns></returns>
        public bool InsertRow(Dictionary<string, string> model)
        {
            bool success;
            string listId;
            string command;
            string values;

            success = false;
            // Buscamos el ID de la lista
            listId = model.FirstOrDefault(m => m.Key == "ListId").Value; 
            if (listId != null)
            {
                try
                {
                    // Construimos la cadena utilizada para insertar la tupla dentro de la tabla que representa a la lista
                    command = string.Format("INSERT INTO DataAppList.{0} (Id", listId);
                    values = "'" + GetNextId(listId).ToString() + "'";
                    using (ListsBLL list = new ListsBLL())
                    {
                        List<ListStructure> structure;
                        // Buscamos la estructura de la lista e iteramos cada elemento (Columna) de la misma
                        structure = list.GetListStructure(listId);
                        foreach (ListStructure column in structure)
                        {
                            string errors = string.Empty;
                            string itemVal = model.FirstOrDefault(m => m.Key == column.ColumnName).Value;
                            itemVal = ((itemVal == null) || (itemVal.Trim() == string.Empty)) ? null : itemVal;
                            // Valor por defecto
                            if (column.DefaultValue != null)
                            {
                                itemVal = itemVal ?? column.DefaultValue;
                            }
                            command += "," + column.ColumnName.Replace(" ", "");
                            values += itemVal != null ? ",'" + itemVal.Replace("'", "''") + "'" : ",NULL";
                        }
                        command += ") VALUES (" + values + ");";
                        db.Database.ExecuteSqlCommand(command);
                        success = true;
                    }
                }
                catch (Exception) { }
            }

            return success;
        }

        /// <summary>
        /// Edita la tupla dentro de la lista indicada por el modelo
        /// </summary>
        /// <param name="model"></param>
        /// <returns></returns>
        public bool EditRow(Dictionary<string, string> model)
        {
            bool success;
            string listId;
            string command;

            success = false;
            // Buscamos el ID de la lista
            listId = model.FirstOrDefault(m => m.Key == "ListId").Value;
            if (listId != null)
            {
                try
                {
                    // Construimos la cadena utilizada para editar la tupla
                    command = string.Format("UPDATE DataAppList.{0} SET ", listId);
                    using (ListsBLL list = new ListsBLL())
                    {
                        List<ListStructure> structure;
                        // Buscamos la estructura de la lista e iteramos cada elemento (Columna) de la misma
                        structure = list.GetListStructure(listId);
                        foreach (ListStructure column in structure)
                        {
                            string errors = string.Empty;
                            string itemVal = model.FirstOrDefault(m => m.Key == column.ColumnName).Value;
                            itemVal = ((itemVal == null) || (itemVal.Trim() == string.Empty)) ? null : itemVal;
                            // Valor por defecto
                            if (column.DefaultValue != null)
                            {
                                itemVal = itemVal ?? column.DefaultValue;
                            }
                            if (itemVal != null)
                            {
                                command += string.Format("{0} = '{1}' , ", column.ColumnName, itemVal.Replace("'", "''"));
                            }
                            else
                            {
                                command += string.Format("{0} = NULL , ", column.ColumnName);
                            }
                        }
                        command = command.Substring(0, command.Length - 2) + "WHERE Id = " + model.FirstOrDefault(m => m.Key == "Id").Value;
                        db.Database.ExecuteSqlCommand(command);
                        success = true;
                    }
                }
                catch (Exception) { }
            }

            return success;
        }

        /// <summary>
        /// Elimina la tupla de la lista indicada por el modelo
        /// </summary>
        /// <param name="model"></param>
        /// <returns></returns>
        public bool DeleteRow(Dictionary<string, string> model)
        {
            bool success;
            string listId;
            string command;

            success = false;
            // Buscamos el ID de la lista
            listId = model.FirstOrDefault(m => m.Key == "ListId").Value;
            if (listId != null)
            {
                try
                {
                    // Buscamos el ID de la tupla a eliminar
                    long columnId = long.Parse(model.FirstOrDefault(m => m.Key == "Id").Value);
                    // Construimos la cadena utilizada para eliminar la tupla
                    command = string.Format("DELETE FROM DataAppList.{0} WHERE Id = {1}", listId, columnId);
                    // Eliminamos la tupla
                    db.Database.ExecuteSqlCommand(command);
                    // Eliminamos todos los archivos adjuntos a esta tupla
                    var attachmentsList = (from e in db.ListsAttachments where e.ListId == listId && e.ColumnId == columnId select e.Attachments.Mask);
                    using (AttachmentsBLL attachments = new AttachmentsBLL())
                    {
                        foreach (string item in attachmentsList)
                            attachments.RemoveFile(listId, columnId, item);
                    }
                    success = true;
                }
                catch (Exception) { }
            }

            return success;
        }

        /// <summary>
        /// Devuelve el proximo id a utilizar para agregar una tupla a una lista
        /// </summary>
        /// <param name="listId">Lista a la que se agregara la tupla</param>
        /// <returns></returns>
        public long GetNextId(string listId)
        {
            long id;
            string command;

            command = string.Format("SELECT TOP 1 Id FROM DataAppList.{0} ORDER BY Id DESC", listId);
            id = db.Database.SqlQuery<long>(command).FirstOrDefault<long>();

            return id + 1;
        }

        /// <summary>
        /// Redondea un numero con punto flotante
        /// </summary>
        /// <param name="value">Valor a redondear</param>
        /// <param name="precision">Presicion o puntos decimales a utilizar</param>
        /// <returns></returns>
        private double TruncateDouble(double value, int precision)
        {
            double step = Math.Pow(10, precision);
            int tmp = (int)Math.Truncate(step * value);
            return tmp / step;
        }

        public void Dispose()
        {
            db.Dispose();
        }
    }
}